import { Metadata } from './Metadata';
import { DCMViewerError } from '../../../DCMViewerError';

/**
 * ATTENTION! This class should never depend on StudyMetadata or SeriesMetadata classes as this could
 * possibly cause circular dependency issues.
 */

const UNDEFINED = 'undefined';
const STRING = 'string';
const STUDY_INSTANCE_UID = 'x0020000d';
const SERIES_INSTANCE_UID = 'x0020000e';

export class BaseInstanceMetadata extends Metadata {
    constructor(data, uid) {
        super(data, uid);
        // Initialize Private Properties
        Object.defineProperties(this, {
            _sopInstanceUID: {
                configurable: true, // configurable so that it can be redefined in sub-classes...
                enumerable: false,
                writable: true,
                value: null
            },
            _imageId: {
                configurable: true, // configurable so that it can be redefined in sub-classes...
                enumerable: false,
                writable: true,
                value: null
            }
        });
        // Initialize Public Properties
        this._definePublicProperties();
    }

    /**
     * Private Methods
     */

    /**
     * Define Public Properties
     * This method should only be called during initialization (inside the class constructor)
     */
    _definePublicProperties() {
        /**
         * Property: this.sopInstanceUID
         * Same as this.getSOPInstanceUID()
         * It's specially useful in contexts where a method call is not suitable like in search criteria. For example:
         * sopInstanceCollection.findBy({
         *   sopInstanceUID: '1.2.3.4.5.6.77777.8888888.99999999999.0'
         * });
         */
        Object.defineProperty(this, 'sopInstanceUID', {
            configurable: false,
            enumerable: false,
            get() {
                return this.getSOPInstanceUID();
            }
        });
    }

    /**
     * Public Methods
     */

    /**
     * Returns the StudyInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
     */
    getStudyInstanceUID() {
        return this.getTagValue(STUDY_INSTANCE_UID, null);
    }

    /**
     * Returns the SeriesInstanceUID of the current instance. This method is basically a shorthand the full "getTagValue" method call.
     */
    getSeriesInstanceUID() {
        return this.getTagValue(SERIES_INSTANCE_UID, null);
    }

    /**
     * Returns the SOPInstanceUID of the current instance.
     */
    getSOPInstanceUID() {
        return this._sopInstanceUID;
    }

    // @TODO: Improve this... (E.g.: blob data)
    getStringValue(tagOrProperty, index, defaultValue) {
        let value = this.getTagValue(tagOrProperty, defaultValue);

        if (typeof value !== STRING && typeof value !== UNDEFINED) {
            value = value.toString();
        }

        return BaseInstanceMetadata.getIndexedValue(value, index, defaultValue);
    }

    // @TODO: Improve this... (E.g.: blob data)
    getFloatValue(tagOrProperty, index, defaultValue) {
        let value = this.getTagValue(tagOrProperty, defaultValue);
        value = BaseInstanceMetadata.getIndexedValue(value, index, defaultValue);

        if (value instanceof Array) {
            value.forEach((val, idx) => {
                value[idx] = parseFloat(val);
            });

            return value;
        }

        return typeof value === STRING ? parseFloat(value) : value;
    }

    // @TODO: Improve this... (E.g.: blob data)
    getIntValue(tagOrProperty, index, defaultValue) {
        let value = this.getTagValue(tagOrProperty, defaultValue);
        value = BaseInstanceMetadata.getIndexedValue(value, index, defaultValue);

        if (value instanceof Array) {
            value.forEach((val, idx) => {
                value[idx] = parseFloat(val);
            });

            return value;
        }

        return typeof value === STRING ? parseInt(value, 10) : value;
    }

    /**
     * @deprecated Please use getTagValue instead.
     */
    getRawValue(tagOrProperty, defaultValue) {
        return this.getTagValue(tagOrProperty, defaultValue);
    }

    /**
     * This function should be overriden by specialized classes in order to allow client libraries or viewers to take advantage of the Study Metadata API.
     * @param tagOrProperty
     * @param defaultValue
     */
    getTagValue(tagOrProperty, defaultValue) {
        /**
         * Please override this method on a specialized class.
         */
        throw new DCMViewerError('InstanceMetadata::getTagValue is not overriden. Please, override it in a specialized class. See InstanceMetadata for example', tagOrProperty, defaultValue);
    }

    /**
     * Compares the current instance with another one.
     * @param {BaseInstanceMetadata} instance An instance of the InstanceMetadata class.
     * @returns {boolean} Returns true if both instances refer to the same instance.
     */
    equals(instance) {
        const self = this;
        return (
            instance === self ||
            (
                instance instanceof BaseInstanceMetadata &&
                instance.getSOPInstanceUID() === self.getSOPInstanceUID()
            )
        );
    }

    /**
     * Check if the tagOrProperty exists
     * @param  {String} tagOrProperty tag or property be checked
     * @return {Boolean}   True if the tag or property exists or false if doesn't
     */
    tagExists(tagOrProperty) {
        /**
         * Please override this method
         */
        throw new DCMViewerError('InstanceMetadata::tagExists is not overriden. Please, override it in a specialized class. See InstanceMetadata for example', tagOrProperty);
    }

    /**
     * Get custom image id of a sop instance
     * @param frame
     * @return {Any}          sop instance image id
     */
    getImageId(frame) {
        /**
         * Please override this method
         */
        throw new DCMViewerError('InstanceMetadata::getImageId is not overriden. Please, override it in a specialized class. See InstanceMetadata for example', frame);
    }

    /**
     * Static Methods
     */

    /**
     * Get an value based that can be index based. This function is called by all getters. See above functions.
     *     - If value is a String and has indexes:
     *         - If undefined index: returns an array of the split values.
     *         - If defined index:
     *             - If invalid: returns defaultValue
     *             - If valid: returns the indexed value
     *      - If value is not a String, returns default value.
     */
    static getIndexedValue(value, index, defaultValue) {
        let result = defaultValue;

        if (typeof value === STRING) {
            const hasIndexValues = value.indexOf('\\') !== -1;

            result = value;

            if (hasIndexValues) {
                const splitValues = value.split('\\');
                if (Metadata.isValidIndex(index)) {
                    const indexedValue = splitValues[index];

                    result = typeof indexedValue !== STRING ? defaultValue : indexedValue;
                } else {
                    result = splitValues;
                }
            }
        }

        return result;
    }
}
